package com.os.core.datasource;

import com.alibaba.druid.pool.DruidDataSource;
import com.alibaba.druid.stat.DruidDataSourceStatManager;
import com.os.common.entity.datasource.DataSource;
import com.os.common.exception.DataSourceConfigException;
import com.os.common.exception.DataSourceCreateException;
import com.os.common.exception.DataSourceNotFoundException;
import com.os.common.utils.MyUtil;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

/**
 * 描述：配置动态数据源
 *
 * @author huxuehao
 **/
public class DynamicDataSource extends AbstractRoutingDataSource {
    private Map<Object, Object> dynamicTargetDataSources;
    /* 连接池连接信息 */
    @Value("${spring.datasource.initial-size}")
    private int initialSize;
    @Value("${spring.datasource.min-idle}")
    private int minIdle;
    @Value("${spring.datasource.max-active}")
    private int maxActive;
    @Value("${spring.datasource.max-wait}")
    private int maxWait;
    @Value("${spring.datasource.time-between-eviction-runs-millis}")
    private long timeBetweenEvictionRunsMillis;
    @Value("${spring.datasource.max-evictable-idle-time-millis}")
    private long maxEvictableIdleTimeMillis;
    @Value("${spring.datasource.validation-query}")
    private String validationQuery;
    @Value("${spring.datasource.test-while-idle}")
    private boolean testWhileIdle;
    @Value("${spring.datasource.test-on-borrow}")
    private boolean testOnBorrow;
    @Value("${spring.datasource.test-on-return}")
    private boolean testOnReturn;
    @Value("${spring.datasource.filters}")
    private String filters;

    public void clearDSCache() {
        this.dynamicTargetDataSources = new HashMap<>();
    }

    /**
     * 检测并创建动态数据源：数据源不存在则创建，存在则创建
     */
    public boolean createDataSourceWithCheck(DataSource dataSource) {
        /* 目标数据源ID */
        String datasourceId = String.valueOf(dataSource.getId());
        /* 备份目标数据源 */
        Map<Object, Object> dynamicTargetDataSourcesBak = this.dynamicTargetDataSources;
        /* 检查目标数据源是否存在 */
        if (dynamicTargetDataSourcesBak.containsKey(datasourceId)) {
            /* 获取数据源*/
            DruidDataSource druidDataSource = (DruidDataSource) dynamicTargetDataSourcesBak.get(datasourceId);
            /* 测试目标数据源 */
            try (Connection connection = druidDataSource.getConnection()) {
                if (connection != null) {
                    return true;
                }
                /* 若数据库连接存在问题，则删除 */
                deleteDataSources(datasourceId);
            } catch (Exception e) {
                /* 若出现异常，则删除目标数据源*/
                deleteDataSources(datasourceId);
            }
        }
        /* 创建数据源 */
        return createDataSource(dataSource);

    }

    /**
     * 删除数据源
     *
     * @param datasourceId 数据源ID
     */
    private void deleteDataSources(String datasourceId) {
        /* 获取当前数据源的map集的备份 */
        Map<Object, Object> dynamicTargetDataSourcesBak = this.dynamicTargetDataSources;
        /* 若当前数据源中存在目标数据源 */
        if (dynamicTargetDataSourcesBak.containsKey(datasourceId)) {
            /* 获取Druid目前维护的数据源 */
            Set<DruidDataSource> druidDataSourceInstances = DruidDataSourceStatManager.getDruidDataSourceInstances();
            /* 遍历Druid数据源 */
            for (DruidDataSource ds : druidDataSourceInstances) {
                /* 当找到目标数据源时 */
                if (datasourceId.equals(ds.getName())) {
                    /* 移除当前数据源的map集备份中的目标数据源 */
                    dynamicTargetDataSourcesBak.remove(datasourceId);
                    /* 移除Druid中的目标数据源 */
                    DruidDataSourceStatManager.removeDataSource(ds);
                    /* 刷新数据源map */
                    setTargetDataSources(dynamicTargetDataSourcesBak);
                    return;
                }
            }
        }
    }

    /**
     * 创建数据源
     */
    private boolean createDataSource(DataSource dataSource) {
        /* 数据源ID */
        String datasourceId = String.valueOf(dataSource.getId());
        /* 用户名 */
        String userName = dataSource.getUserName();
        /* 密码 */
        String password = dataSource.getPassword();
        /* url */
        String url = dataSource.getUrl();
        /* 驱动 */
        String driveClass = dataSource.getDrive();
        /* 测试数据源配置 */
        if(!testDatasourceConfig(driveClass,url,userName,password)) {
            throw new DataSourceConfigException();
        }
        /* 创建数据源 */
        if(!this.createDataSource(datasourceId, driveClass, url, userName, password)) {
            throw new DataSourceCreateException();
        }
        return true;
    }

    /**
     * 测试数据源配置是否存在问题
     * @param driveClass 驱动
     * @param url        url
     * @param username   用户名
     * @param password   密码
     */
    private boolean testDatasourceConfig(String driveClass, String url, String username, String password) {
        Connection connection = null;
        try {
            Class.forName(driveClass);
            connection = DriverManager.getConnection(url, username, password);
            return connection != null;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        } finally {
            if (connection != null) {
                try {
                    connection.close();
                } catch (SQLException ignored) {}
            }
        }
    }

    /**
     * 创建数据源
     * @param key            数据源key
     * @param driveClass     驱动
     * @param url            url
     * @param username       用户名
     * @param password       密码
     */
    private boolean createDataSource(String key, String driveClass, String url, String username, String password) {
        Connection connection = null;
        try {
            /* 排除连接不上的错误*/
            Class.forName(driveClass);
            connection = DriverManager.getConnection(url, username, password);// 相当于连接数据库
            if (connection == null) {
                return false;
            }
            /* 创建数据源 */
            DruidDataSource druidDataSource = new DruidDataSource();
            druidDataSource.setName(key);
            druidDataSource.setDriverClassName(driveClass);
            druidDataSource.setUrl(url);
            druidDataSource.setUsername(username);
            druidDataSource.setPassword(password);
            /* 初始化时建立物理连接的个数。初始化发生在显示调用init方法，或者第一次getConnection时*/
            druidDataSource.setInitialSize(initialSize);
            /* 最大连接池数量 */
            druidDataSource.setMaxActive(maxActive);
            /* 获取连接时最大等待时间，单位毫秒。当链接数已经达到了最大链接数的时候，应用如果还要获取链接就会出现等待的现象，等待链接释放并回到链接池，如果等待的时间过长就应该踢掉这个等待，不然应用很可能出现雪崩现象 */
            druidDataSource.setMaxWait(maxWait);
            /* 最小连接池数量 */
            druidDataSource.setMinIdle(minIdle);
            /* 申请连接时执行validationQuery检测连接是否有效，这里建议配置为TRUE，防止取到的连接不可用 */
            druidDataSource.setTestOnBorrow(testOnBorrow);
            /* 建议配置为true，不影响性能，并且保证安全性。申请连接的时候检测，如果空闲时间大于timeBetweenEvictionRunsMillis，执行validationQuery检测连接是否有效。 */
            druidDataSource.setTestWhileIdle(testWhileIdle);
            druidDataSource.setTestOnReturn(testOnReturn);
            /* 用来检测连接是否有效的sql，要求是一个查询语句。如果validationQuery为null，testOnBorrow、testOnReturn、testWhileIdle都不会起作用。 */
            druidDataSource.setValidationQuery(validationQuery);
            /* 属性类型是字符串，通过别名的方式配置扩展插件，常用的插件有：监控统计用的filter:stat日志用的filter:log4j防御sql注入的filter:wall */
            druidDataSource.setFilters(filters);
            /* 配置间隔多久才进行一次检测，检测需要关闭的空闲连接，单位是毫秒 */
            druidDataSource.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis);
            /* 配置一个连接在池中最小生存的时间，单位是毫秒 */
            druidDataSource.setMinEvictableIdleTimeMillis(maxEvictableIdleTimeMillis);
            /* 打开druid.keepAlive之后，当连接池空闲时，池中的minIdle数量以内的连接，空闲时间超过minEvictableIdleTimeMillis，则会执行keepAlive操作，即执行druid.validationQuery指定的查询SQL，一般为select * from dual，只要minEvictableIdleTimeMillis设置的小于防火墙切断连接时间，就可以保证当连接空闲时自动做保活检测，不会被防火墙切断 */
            druidDataSource.setKeepAlive(true);
            /* 是否移除泄露的连接/超过时间限制是否回收。 */
            druidDataSource.setRemoveAbandoned(true);
            /* 泄露连接的定义时间(要超过最大事务的处理时间)；单位为秒。这里配置为1小时 */
            druidDataSource.setRemoveAbandonedTimeout(3600);
            /* 移除泄露连接发生是是否记录日志 */
            druidDataSource.setLogAbandoned(true);
            druidDataSource.init();
            this.dynamicTargetDataSources.put(key, druidDataSource);
            /* 将map赋值给父类的TargetDataSources */
            setTargetDataSources(this.dynamicTargetDataSources);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        } finally {
            if (connection != null) {
                try {
                    connection.close();
                } catch (SQLException ignored) {}
            }
        }
    }

    /**
     * 设置当前要使用的数据源，key是数据源的唯一表示，value是数据源实体.
     * 通过 super.setTargetDataSources() 将当前已经存在的数据源map集，传递给 AbstractRoutingDataSource 的 targetDataSources
     * <p>
     * 该方法是在动态数据源被初始化后被调用（在当前类中），不能说是在数据源被切换时被调用，以为数据源切换有两种情况：
     * 1. 数据源没有被初始化，那么会初始化数据源即createDataSource()，然后修改DBContextHolder
     * 2. 数据源已经被初始化，那么会直接修改DBContextHolder
     */
    @Override
    public void setTargetDataSources(@NotNull Map<Object, Object> targetDataSources) {
        super.setTargetDataSources(targetDataSources);
        super.afterPropertiesSet(); // 利用afterPropertiesSet()将 targetDataSources 赋值给 resolvedDataSources
        this.dynamicTargetDataSources = targetDataSources;
    }

    /**
     * 设置默认数据源，默认数据源不需要key
     * 通过 super.setDefaultTargetDataSource() 将默认数据源，传递给 AbstractRoutingDataSource 的 defaultTargetDataSource
     * <p>
     * 该方法是在DynamicDataSource被实例化过程中被调用（com.os.core.config.DruidDBConfig =>  dynamicDataSource()）
     */
    @Override
    public void setDefaultTargetDataSource(@NotNull Object defaultTargetDataSource) {
        super.setDefaultTargetDataSource(defaultTargetDataSource);
    }

    /**
     * 该方法是实现动态数据源切换的核心方法。其的作用就是拿到目标数据源的key,从而获取到数据源。
     * <p>
     * 该方法中我们可以看到，数据源key来源于ThreadLocal，所以我们如何想要切换数据源，那么
     * 我们只需要修改ThreadLocal（DBContextHolder）中的数据源key即可
     */
    @Override
    protected Object determineCurrentLookupKey() {
        /* 从ThreadLocal中尝试获取当前线程使用的数据源的key */
        String datasourceKey = DBContextHolder.getDataSource();
        /* 如果ThreadLocal中不为空 */
        if (!MyUtil.isEmpty(datasourceKey)) {
            /* 获取现在维护的数据源map集 */
            Map<Object, Object> dynamicTargetDataSourcesBak = this.dynamicTargetDataSources;
            /* 若ThreadLocal中的数据源ke不存在于目前项目所创建的数据源map集中，则抛出异常 */
            if (!dynamicTargetDataSourcesBak.containsKey(datasourceKey)) {
               throw new DataSourceNotFoundException();
            }
        }
        return datasourceKey;
    }
}
 